/*
 * Copyright (c) 2011-2022, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sagacity.framework.jdbc.mybatis.meta;

import com.sagacity.framework.util.CollectionUtil;
import com.sagacity.framework.util.StringUtil;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.io.IOUtils;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;

import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 数据库表反射信息
 *
 * @author hubin
 * @since 2016-01-23
 */
@Data
@Setter(AccessLevel.PACKAGE)
@Accessors(chain = true)
public class TableInfo implements Serializable {

    /**
     * 实体类型
     */
    private Class<?> entityType;
    /**
     * 表名称
     */
    private String tableName;
    /**
     * 表主键ID 字段名
     */
    private String keyColumn;
    /**
     * 表主键ID 属性名
     */
    private String keyProperty;

    /**
     * 表主键ID 字段名
     */
    private String logicColumn;
    /**
     * 表主键ID 属性名
     */
    private String logicProperty;

    /**
     * 表字段信息列表
     */
    private List<TableFieldInfo> fieldList;
    /**
     * 命名空间 (对应的 mapper 接口的全类名)
     */
    private String currentNamespace;
    /**
     * MybatisConfiguration 标记 (Configuration内存地址值)
     */
    @Getter
    @Setter(AccessLevel.NONE)
    private Configuration configuration;

    public TableInfo(Class<?>  entityType){
        this.entityType = entityType;
    }

    public XNode search() {
        String xml = "<select id=\"search\" parameterType=\"com.sagacity.framework.api.model.request.GenericQueryBO\" resultType=\"" + entityType.getCanonicalName() + "\">\n" +
                "SELECT\n" +
                " <choose>\n" +
                "\t<when test=\"includes != null and includes.size() != 0\">\n" +
                "\t\t<foreach collection=\"includes\" item=\"value\" separator=\",\">\n" +
                "\t\t\t${value}\n" +
                "\t\t</foreach>\n" +
                "\t</when>\n" +
                "\t<otherwise>\n" +
                baseColumnList()+"\n" +
                "\t</otherwise>\n" +
                "</choose>\n" +
                "FROM " + tableName + "\n" +searchWhereTrim()+
                "</select>";
        return getXNode(xml,"select");
    }


    /**
     * 复杂查询条件
     * @return 复杂查询方法条件
     */
    private String searchWhereTrim() {
        return "<trim prefix=\"WHERE\" prefixOverrides=\"AND|OR\">\n" +
                /* 实体类相等*/
                "<if test=\"entity != null\">\n"+
                fieldList.stream().map(field -> "    <if test=\"" + testEntityField(field) + "\">" + andEntityEqColumn(field) + "</if>")
                        .collect(Collectors.joining("\n"))
                +"</if>\n"
                + searchWhereConditions()+"\n"
                + searchOrderBy()
                +"\n</trim>";
    }

    /**
     * 根据指定字段排序
     */
    private String searchOrderBy() {
        return "";
    }

    private String searchWhereConditions() {
        return "<trim suffixOverrides=\"AND|OR\">\n" +
                "            <if test=\"conditions != null and conditions.criteria != null and conditions.criteria.size() != 0\">\n" +
                "                <foreach collection=\"conditions.criteria\" index=\"index\" item=\"criterion\" open=\"AND (\" close=\")\">\n" +
                "                    <if test=\"criterion.prefix != null and criterion.prefix != ''\">\n" +
                "                        ${criterion.prefix}\n" +
                "                    </if>\n" +
                "                    ${criterion.fieldName} ${criterion.condition}\n" +
                "                    <choose>\n" +
                "                        <when test=\"criterion.condition == 'IN'.toString() or criterion.condition == 'NOT IN'.toString()\">\n" +
                "                            <foreach collection=\"criterion.fieldValue\" item=\"value\" open=\"(\" close=\")\" separator=\",\">\n" +
                "                                #{value}\n" +
                "                            </foreach>\n" +
                "                        </when>\n" +
                "                        <when test=\"criterion.condition == 'BETWEEN'.toString()\">\n" +
                "                            #{criterion.fieldValue[0]} AND #{criterion.fieldValue[1]}\n" +
                "                        </when>\n" +
                "                        <when test=\"criterion.condition == 'IS NULL'.toString() or criterion.condition == 'IS NOT NULL'.toString()\">\n" +
                "\n" +
                "                        </when>\n" +
                "                        <otherwise>\n" +
                "                            #{criterion.fieldValue}\n" +
                "                        </otherwise>\n" +
                "                    </choose>\n" +
                "                    <if test=\"criterion.suffix != null and criterion.suffix != ''\">\n" +
                "                        ${criterion.suffix}\n" +
                "                    </if>\n" +
                "                    <if test=\"criterion.connect != null and criterion.connect != ''\">\n" +
                "                        ${criterion.connect}\n" +
                "                    </if>\n" +
                "                </foreach>\n" +
                "            </if>\n" +
                "        </trim>";
    }

    private String eqEntityColumn(TableFieldInfo field) {
        return field.getColumn() + " = #{" + "entity."+field.getProperty() + "}";
    }

    private String testEntityField(TableFieldInfo field) {
        return "entity."+field.getProperty() + " != null " + (field.isCharSequence() ? "and " + "entity."+field.getProperty() + " !=''" : "");
    }

    public XNode selectById(){
        String xml = "<select id=\"selectById\" parameterType=\"java.io.Serializable\" resultType=\"" + entityType.getCanonicalName() + "\">\n" +
                "SELECT\n" +
                "    " + baseColumnList() + "\n" +
                "FROM " + tableName + "\n" +whereEqId()+
                "</select>";
        return getXNode(xml,"select");
    }

    private String whereEqId() {
        return " WHERE "+keyColumn + " = #{"+keyProperty+"}\n";
    }

    public XNode selectBatchIds(){
        String xml = "<select id=\"selectBatchIds\" parameterType=\"list\" resultType=\"" + entityType.getCanonicalName() + "\">\n" +
                "SELECT\n" +
                "    " + baseColumnList() + "\n" +
                "FROM " + tableName + "\n" +
                "        <foreach item=\"id\" index=\"index\" collection=\"idList\"\n" +
                "                 open=\"WHERE "+keyColumn+" in (\" separator=\",\" close=\")\">\n" +
                "            #{id}\n" +
                "        </foreach>\n" +
                "</select>";
        return getXNode(xml,"select");
    }

    public XNode selectCount(){
        String xml = "<select id=\"selectCount\" parameterType=\"" + entityType.getCanonicalName() + "\" resultType=\"long\">\n" +
                "SELECT\n" +
                "    COUNT(*)\n" +
                "FROM " + tableName + "\n" +
                whereTrim() + "\n" +
                "</select>";
        return getXNode(xml,"select");
    }

    public XNode selectList(){
        String xml = "<select id=\"selectList\" parameterType=\"" + entityType.getCanonicalName() + "\" resultType=\"" + entityType.getCanonicalName() + "\">\n"
                + "SELECT\n" +
                "    " + baseColumnList() + "\n" +
                "FROM " + tableName + "\n" +
                whereTrim() + "\n" +
                "</select>";
        return getXNode(xml,"select");
    }

    public XNode selectOne(){
        String xml = "<select id=\"selectOne\" parameterType=\"" + entityType.getCanonicalName() + "\" resultType=\"" + entityType.getCanonicalName() + "\">\n"
                + "SELECT\n" +
                "    " + baseColumnList() + "\n" +
                "FROM " + tableName + "\n" +
                whereTrim() + "\n" +
                "LIMIT 1" +
                "</select>";
        return getXNode(xml,"select");
    }

    private static XNode getXNode(String xml,String expression) {
        if(StringUtil.isBlank(xml)){
            return null;
        }
        InputStream inputStream = IOUtils.toInputStream(xml, StandardCharsets.UTF_8);
        XPathParser xpathParser = new XPathParser(inputStream);
        List<XNode> xNodes = xpathParser.evalNodes(expression);
        if(CollectionUtil.isEmpty(xNodes)){
            return null;
        }
        return xNodes.get(0);
    }

    private String baseColumnList() {
        return fieldList.stream().map(TableFieldInfo::getColumn).collect(Collectors.joining(","));
    }

    private String whereTrim() {
        return "<trim prefix=\"WHERE\" prefixOverrides=\"AND|OR\">\n" +
                fieldList.stream().map(field -> "    <if test=\"" + testField(field) + "\">" + andEqColumn(field) + "</if>")
                        .collect(Collectors.joining("\n"))
                +"\n</trim>";
    }

    private String andEntityEqColumn(TableFieldInfo field) {
        return "AND " +eqEntityColumn(field);
    }

    private String andEqColumn(TableFieldInfo field) {
        return "AND " +eqColumn(field);
    }
    private String eqColumn(TableFieldInfo field) {
        return field.getColumn() + " = #{" + field.getProperty() + "}";
    }

    private String testField(TableFieldInfo field) {
        return field.getProperty() + " != null " + (field.isCharSequence() ? "and " + field.getProperty() + " !=''" : "");
    }

    public XNode insert() {
        String xml = "<insert id=\"insert\" parameterType=\"" + entityType.getCanonicalName() + "\">\n"
                + "INSERT INTO "+tableName+" ( \n" +
                "    " + baseColumnList() + "\n" +
                ") VALUES (\n"+
                insertProperty()+
                "\n)\n"+
                "</insert>";

        return getXNode(xml,"insert");
    }

    private String insertProperty() {
        return fieldList.stream().map(field->"#{"+field.getProperty()+"}").collect(Collectors.joining(","));
    }

    public XNode updateById() {
        String xml = "<update id=\"updateById\" parameterType=\"" + entityType.getCanonicalName() + "\">\n"
                + "UPDATE "+tableName+" \n" +
                "    " + updateSelectColumn() + "\n" +
                whereEqId()+
                "</update>";

        return getXNode(xml,"update");
    }

    private String updateSelectColumn() {
        return "<trim prefix=\"SET\" suffixOverrides=\",\">\n" +
                fieldList.stream().filter(fieldInfo -> !fieldInfo.isPrimaryKey()).map(field -> "    <if test=\"" + testField(field) + "\">" + eqColumn(field) + ",</if>")
                        .collect(Collectors.joining("\n"))
                +"\n</trim>";
    }

    public XNode disableById() {
        if(StringUtil.isNoneEmpty(logicColumn,logicProperty)){
            String xml = "<update id=\"disableById\" parameterType=\"" + entityType.getCanonicalName() + "\">\n"
                    + "UPDATE "+tableName+" \n" +
                    "    " + updateDisable() + "\n" +
                    whereEqId()+
                    "</update>";

            return getXNode(xml,"update");
        }
        return null;
    }


    /**
     * 逻辑删除可能需要更新的字段
     */
    private List<String> disableProperties = Arrays.asList("modifyBy", "modifyDate", "updatedBy", "updatedDate", "traceId");

    /**
     * 逻辑删除
     *
     * @return 逻辑删除字段
     */
    private String updateDisable() {
        return "<trim prefix=\"SET\" suffixOverrides=\",\">\n" +
                fieldList.stream().filter(fieldInfo -> {
                            return !fieldInfo.isPrimaryKey()
                                    && (fieldInfo.isLogicDelete()
                                    || disableProperties.contains(fieldInfo.getProperty()));
                                }
                        )
                        .map(field -> "    <if test=\"" + testField(field) + "\">" + eqColumn(field) + ",</if>")
                        .collect(Collectors.joining("\n"))
                + "\n</trim>";
    }

    public XNode deleteById() {
        String xml = "<delete id=\"deleteById\" parameterType=\"" + entityType.getCanonicalName() + "\">\n"
                + "DELETE FROM "+tableName+" "+whereEqId()+
                "</delete>";

        return getXNode(xml,"delete");
    }
}
